package yhao.infra.web.common.enumsmap;


import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import yhao.infra.web.common.enumsmap.annotation.OptionAttribute;
import yhao.infra.web.common.enumsmap.annotation.OptionText;
import yhao.infra.web.common.enumsmap.annotation.OptionValue;
import yhao.infra.web.common.enumsmap.annotation.Selectable;

import java.lang.reflect.Field;
import java.util.*;


/**
 * 初始化并缓系统中所有枚举到下拉框转换关系
 * 所有的枚举到下拉框的对应关系，只会在系统启动的时候做一次转换，然后缓存，不会每次调用的时候都转换
 *
 * @author wuweipeng
 * @author liwan
 * @date 2021/7/20 9:16
 */

public class ApplicationSelectInitBean implements InitializingBean {
    private final static Logger logger = LoggerFactory.getLogger(ApplicationSelectInitBean.class);
    // 包的扫描工具类
    private final ApplicationPackageScanner scanner;

    // 用以缓存系统中所有的从枚举转换成下拉框对象
    private final Map<Key, List<SelectOption>> enumClassSelectCacheMap = new HashMap<>();

    private static final String ENUM_DEFAULT_TEXT_KEY = "alias";

    private static final String ENUM_DEFAULT_VALUE_KEY = "value";

    public ApplicationSelectInitBean(ApplicationPackageScanner scanner) {
        this.scanner = scanner;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Set<Class<?>> selectEnumClazz = this.scanner.scanPackages();
        try {
            if (selectEnumClazz.isEmpty())
                return;

            for (Class<?> clazz : selectEnumClazz) {
                if (clazz.isEnum()) {
                    Enum<?>[] values = (Enum<?>[]) clazz.getEnumConstants();
                    List<SelectOption> options = new ArrayList<>(values.length);

                    for (Enum<?> objEnum : values) {
                        SelectOption option = new SelectOption(null, null);
                        // 反射获取枚举类的属性，转换成 SelectOption 对象
                        Field[] fields = objEnum.getClass().getDeclaredFields();

                        // 下面这三个值填充步骤可以抽取出接口，后续如果更复杂之后可以优化
                        // 第一次填充值，先根据枚举中配置的注解来填充值（优先级最高，如果有配置注解，则后面两步被忽略）
                        this.fillValueByAnnotation(option, objEnum, fields);

                        // 第二次值的填充，如果没有配置相应的注解，根据枚举的属性名来填充（优先级其次，如果没有注解，则根据属性名来获取值）
                        this.fillValueByPropertyName(option, objEnum, fields);

                        // 第三次填充，直接根据枚举的值来填充（优先级最低，如果前两步都无法获取到值才被使用）
                        this.fillValueByDefault(option, objEnum);

                        options.add(option);
                    }

                    // 缓存该下拉框
                    Key cacheKey = new Key(clazz);

                    // 带有@Selectable注解的名称不能相同
                    if (this.enumClassSelectCacheMap.containsKey(cacheKey))
                        throw new IllegalArgumentException(String.format("Duplication select enum cache name: %s & %s", clazz.getName(), this.findExistsKey(clazz.getSimpleName())));

                    this.enumClassSelectCacheMap.put(cacheKey, options);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(),e);
        }
    }

    private String findExistsKey(String clazzName) {
        return this.enumClassSelectCacheMap.keySet().stream().filter(key -> key.clazzName.equals(clazzName)).map(Key::getClazz).map(Class::getName).findFirst().orElse(null);
    }


    private void fillValueByAnnotation(SelectOption option, Enum<?> objEnum, Field[] fields) throws Exception {
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(OptionText.class)) {
                option.setName(field.get(objEnum));
                continue;
            }
            if (field.isAnnotationPresent(OptionValue.class)) {
                option.setValue(field.get(objEnum));
            }
            if (field.isAnnotationPresent(OptionAttribute.class)) {
                OptionAttribute oa = field.getAnnotation(OptionAttribute.class);
                String key = StringUtils.isNoneBlank(oa.name()) ? oa.name() : field.getName();
                option.addAttr(key, field.get(objEnum));
            }
        }
    }


    private void fillValueByPropertyName(SelectOption option, Enum<?> objEnum, Field[] fields) throws Exception {
        if (option.hasFull())
            return;

        for (Field field : fields) {
            field.setAccessible(true);
            if (ENUM_DEFAULT_TEXT_KEY.equals(field.getName()) && option.getName() == null) {
                option.setName(field.get(objEnum));
                continue;
            }
            if (ENUM_DEFAULT_VALUE_KEY.equals(field.getName()) && option.getValue() == null) {
                option.setValue(field.get(objEnum));
            }
        }
    }


    private void fillValueByDefault(SelectOption option, Enum<?> objEnum) {
        if (option.hasFull())
            return;

        if (option.getName() == null) {
            option.setName(objEnum.name());
        }
        if (option.getValue() == null) {
            option.setValue(objEnum.name());
        }
    }


    public List<SelectOption> getSelect(Class<? extends Enum<?>> clazzEnum) {
        Key cacheKey = new Key(clazzEnum);
        if (this.enumClassSelectCacheMap.containsKey(cacheKey)) {
            return this.enumClassSelectCacheMap.get(cacheKey);
        }
        return new ArrayList<>();
    }

    public List<SelectOption> getSelect(String clazzName) {
        Validate.notBlank(clazzName, "枚举名不能为空");

        Key cacheKey = new Key(clazzName);
        if (this.enumClassSelectCacheMap.containsKey(cacheKey)) {
            return this.enumClassSelectCacheMap.get(cacheKey);
        }
        return new ArrayList<>();
    }


    private static class Key {

        private Class<?> clazz;

        private String clazzName;

        static Key EMPTY = new Key("");

        Key(Class<?> clazz) {
            this.clazz = clazz;
            this.clazzName = this.getClazzKeyName(clazz);
        }

        Key(String clazzName) {
            this.clazzName = clazzName;
        }

        private String getClazzKeyName(Class<?> clazz) {
            Selectable selectable = clazz.getAnnotation(Selectable.class);
            String alias = selectable.alias();
            return StringUtils.isNotBlank(alias) ? alias : clazz.getSimpleName();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || this.getClass() != o.getClass()) return false;

            Key key = (Key) o;

            return this.clazzName.equals(key.clazzName);
        }

        @Override
        public int hashCode() {
            return this.clazzName.hashCode();
        }

        public Class<?> getClazz() {
            return clazz;
        }

        public void setClazz(Class<?> clazz) {
            this.clazz = clazz;
        }

        public String getClazzName() {
            return clazzName;
        }

        public void setClazzName(String clazzName) {
            this.clazzName = clazzName;
        }

        public static Key getEMPTY() {
            return EMPTY;
        }

        public static void setEMPTY(Key EMPTY) {
            Key.EMPTY = EMPTY;
        }
    }

}
